﻿using System;
using System.Collections.Generic;
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
using Word = Microsoft.Office.Interop.Word;
using System.IO;

namespace OLEOrphanPrevention.ExcelAddin._1
{
    public partial class ThisAddIn
    {
        /// <summary>
        /// CLASS: To assist with keeping track of Word instances
        /// </summary>
        class DocumentTrack
        {
            public string WorkbookName;
            public string DocumentName;
        }

        System.Windows.Forms.Timer tmrCheckInstances = null;
        List<DocumentTrack> activatedDocuments = new List<DocumentTrack>();
        Word.Application wdApp = null;

        /// <summary>
        /// STARTUP: Hook to the Excel events and start our timer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
            // hook to both events - required, even though we only use the
            // Workbook Activate event. Without the WorkbookOpen event hooked
            // it appears that Excel will still throw error that it needs to
            // be installed in order to open OLE objects
            Application.WorkbookActivate += new Excel.AppEvents_WorkbookActivateEventHandler(Application_WorkbookActivate);
            Application.WorkbookOpen += new Excel.AppEvents_WorkbookOpenEventHandler(Application_WorkbookOpen);
            // Initialize and start our timer which we will use to check to
            // make sure all instances or Word/Excel OLE instances are still
            // valid...
            tmrCheckInstances = new System.Windows.Forms.Timer();
            tmrCheckInstances.Tick += new EventHandler(tmrCheckInstances_Tick);
            tmrCheckInstances.Interval = 1000; // 1 - second
            tmrCheckInstances.Enabled = true;
            tmrCheckInstances.Start();
        }

        /// <summary>
        /// Application_WorkbookOpen - We have to have this to release the COM object only
        /// </summary>
        /// <param name="Wb"></param>
        void Application_WorkbookOpen(Excel.Workbook Wb)
        {
            Marshal.ReleaseComObject(Wb);
        }

        /// <summary>
        /// tmrCheckInstances_Tick - Check to see if all our OLE instances
        /// where Excel is opened as an embed to a Word document are still
        /// open. If they are, then nothing happens, if they are no longer
        /// valid, then we close the Excel instance
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void tmrCheckInstances_Tick(object sender, EventArgs e)
        {
            // are there any documents in the list we are tracking...
            if (activatedDocuments.Count > 0)
            {
                // loop through all the documents we are tracking in rverse order
                // we do this so we can remove an item from the collection without
                // causing an error - e.g. NO ForEach...
                for (int i = activatedDocuments.Count - 1; i >= 0; i-- )
                {
                    DocumentTrack t = activatedDocuments[i];
                    try
                    {
                        // try to access the document from the collection
                        // if wdApp is invalid, or the Document name is no
                        // longer available in that instance of Word then
                        // we will fail and fall into the CATCH
                        object o = wdApp.Documents[t.DocumentName];
                    }
                    catch
                    {
                        // here we assume our tracked documet in Word is closed
                        // or no longer valid. So we will then close the related
                        // Excel workbook. First, loop through the workbooks:
                        foreach (Excel.Workbook wb in Application.Workbooks)
                        {
                            // if we find it...
                            if (wb.Name == t.WorkbookName)
                            {
                                // close it
                                wb.Close(Excel.XlSaveAction.xlDoNotSaveChanges);
                                // and then remove our tracking
                                activatedDocuments.Remove(t);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Application_WorkbookActivate - This is where the instance tracking starts.
        /// We look at the name of the workbook when it is activated. If it has a
        /// .docx in the name, then we assume it is from an embedded instance from Word.
        /// We grab that instance of Word and then we find the document from the Workbook
        /// title and we set a reference to it. We then add that to the tracked 
        /// collection which the timer then checks on.
        /// </summary>
        /// <param name="Wb"></param>
        void Application_WorkbookActivate(Excel.Workbook Wb)
        {
            // are we opened from Word?
            if (Wb.Name.ToLower().EndsWith(".docx") ||
                Wb.Name.ToLower().EndsWith(".doc") ||
                Wb.Name.ToLower().EndsWith(".dotm") ||
                Wb.Name.ToLower().EndsWith(".dot"))
            {
                try
                {
                    // grab the current runing instance of Word
                    wdApp = (Word.Application)Marshal.GetActiveObject("Word.Application");
                }
                catch { }

                Word.Document doc = null;
                int pos = 0;
                string name = Wb.Name;
                // while we do not have a reference to the document we will continue
                // to trim the name of the Workbook to get at the Docuemnt name.
                // Typically it is called "Worksheet in .... .docx"
                // so we are trying to access just the name of the .docx
                while (doc == null)
                {
                    // is there a valid Word instance - if not - stop
                    if (wdApp == null)
                        break;

                    // try to access the Word document with the name...
                    try
                    {
                        doc = wdApp.Documents[name];
                    }
                    catch { }
                    // if we failed, the doc is still null, so start trimming
                    // up the name from space to space...
                    if (doc == null)
                    {
                        // if there are no more spaces, then we do not
                        // have a vlid instance of Word or there is no document
                        // with the name - so stop...
                        if (name.IndexOf(" ") == -1)
                            break;

                        // get the name starting at the next space
                        pos = name.IndexOf(" ") + 1;
                        name = name.Substring(pos);
                    }
                }
                // now we should have a reference to the parent document
                // we will let the timer keep checking to see if it is ever
                // closed
                if (doc != null)
                {
                    // track this one...
                    activatedDocuments.Add(new DocumentTrack()
                        {
                            DocumentName = name,
                            WorkbookName = Wb.Name
                        });
                }
            }
            // important to prevent corruption
            Marshal.ReleaseComObject(Wb);
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion
    }
}

